﻿namespace ap2.Phone.Core.Helpers
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Markup;
    using System.Windows.Media;

    [ContentProperty("Binding")]
    public static class BindingHelper
    {

        #region Binding (Attached DependencyProperty)

        public static RelativeSourceBase GetBinding(DependencyObject obj)
        {
            return (RelativeSourceBase)obj.GetValue(BindingProperty);
        }

        public static void SetBinding(DependencyObject obj, RelativeSourceBase value)
        {
            obj.SetValue(BindingProperty, value);
        }

        public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached("Binding", typeof(RelativeSourceBase), typeof(BindingHelper), new PropertyMetadata(null, OnBinding));

        private static void OnBinding(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement targetElement = depObj as FrameworkElement;

            if (targetElement != null)
            {
                // attach loading event
                targetElement.Loaded += new RoutedEventHandler(targetElement_Loaded);
            }
        }

        #endregion

        #region Private methods

        private static void targetElement_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                FrameworkElement targetElement = sender as FrameworkElement;

                // release handler to prevent memory leaks
                targetElement.Loaded -= new RoutedEventHandler(targetElement_Loaded);

                RelativeSourceBase bindings = GetBinding(targetElement);


                if (bindings is RelativeSourceBinding)
                {
                    // get the binding configuration
                    RelativeSourceBinding bindingConfiguration = bindings as RelativeSourceBinding;
                    ProcessBinding(targetElement, bindingConfiguration);
                }
                else if (bindings is BindingList)
                {
                    // get the binding configuration
                    BindingList list = bindings as BindingList;

                    foreach (RelativeSourceBinding bindingConfiguration in list)
                    {
                        ProcessBinding(targetElement, bindingConfiguration);
                    }
                }

            }
            catch (Exception ex)
            {
                // ignore this exception, because the SL binding engine does not throw exceptions when a binding is wrong.
            }
        }


        private static void ProcessBinding(FrameworkElement targetElement, RelativeSourceBinding bindingConfiguration)
        {

            if (bindingConfiguration.RelativeMode == RelativeSourceMode.FindAncestor &&
                !string.IsNullOrEmpty(bindingConfiguration.AncestorType))
            {
                // navigate up the tree to find the type
                DependencyObject currentObject = VisualTreeHelper.GetParent(targetElement);

                DependencyObject candidate = null;
                DependencyObject ancestor = null;

                while (true)
                {
                    if (currentObject == null)
                    {
                        break;
                    }

                    Type currentType = currentObject.GetType();

                    while (currentType != null && currentType.IsSubclassOf(typeof(DependencyObject)))
                    {
                        if (currentType.FullName == bindingConfiguration.AncestorType)
                        {
                            ancestor = currentObject;
                            break;
                        }

                        // for types in assemblies System.Windows, System.Windows.Controls, System.Windows.Controls.Data, etc, 
                        // its possible to define just the class name instead of the full class name including the namespace.
                        if (candidate == null && currentType.Name == bindingConfiguration.AncestorType && currentType.Assembly.FullName.StartsWith("System.Windows"))
                        {
                            // the name of the element is matching, but it is not the fullname.
                            // remeber the element in case if no element is matching to the ancestor type name
                            candidate = currentObject;
                        }

                        // next type up the hierarchy
                        currentType = currentType.BaseType;
                    }

                    // next parent                    
                    currentObject = VisualTreeHelper.GetParent(currentObject);
                }

                // concrete
                if (ancestor == null)
                {
                    ancestor = candidate;
                }

                if (ancestor != null && ancestor is FrameworkElement)
                {
                    // bind them
                    CreateBinding(targetElement, ancestor, bindingConfiguration);
                }
            }
            else if (bindingConfiguration.RelativeMode == RelativeSourceMode.ParentDataContext)
            {
                object currentDataContext = targetElement.DataContext;

                // navigate up the tree to find the parent datacontext
                DependencyObject currentObject = VisualTreeHelper.GetParent(targetElement);

                while (true)
                {
                    if (currentObject == null)
                        break;

                    FrameworkElement fe = currentObject as FrameworkElement;

                    if (fe != null)
                    {
                        if (fe.DataContext != null && fe.DataContext != currentDataContext)
                        {
                            // bind them
                            CreateBinding(targetElement, fe.DataContext, bindingConfiguration);
                            break;
                        }
                    }

                    // next parent                    
                    currentObject = VisualTreeHelper.GetParent(currentObject);
                }

            }
        }


        private static List<string> GetClassNames(Type type)
        {
            List<string> result = new List<string>();

            // check
            if (type == null && type.IsSubclassOf(typeof(DependencyObject)))
                return result;

            // process
            do
            {
                result.Add(type.FullName);
                type = type.BaseType;
            } while (type != null && type.IsSubclassOf(typeof(DependencyObject)));

            // return
            return result;
        }

        private static void CreateBinding(FrameworkElement targetElement, object sourceElement, RelativeSourceBinding bindingConfiguration)
        {
            // input check
            if (targetElement == null)
                return;
            if (sourceElement == null)
                return;
            if (bindingConfiguration == null)
                return;

            // check binding configuration
            // ...target property must be set
            if (string.IsNullOrEmpty(bindingConfiguration.TargetProperty))
                return;
            // ...path property must be set
            //if (string.IsNullOrWhiteSpace(bindingConfiguration.Path))
            //    return;



            // support of attached property binding syntax: TargetProperty='(Grid.Row)'
            string targetPropertyName = (bindingConfiguration.TargetProperty + "").Trim().TrimStart('(').TrimEnd(')') + "Property";

            // find the target dependency property
            DependencyProperty targetDependencyProperty = null;
            if (targetPropertyName.Contains("."))
            {
                // it is an attached dependency property
                string[] parts = targetPropertyName.Split('.');

                if (parts.Length == 2 && !string.IsNullOrEmpty(parts[0]) && !string.IsNullOrEmpty(parts[1]))
                {
                    Type attachedType = TypeLoader.GetType(parts[0], bindingConfiguration.TargetNamespace);

                    if (attachedType != null)
                    {
                        FieldInfo[] targetFields = attachedType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
                        FieldInfo targetDependencyPropertyField = targetFields.FirstOrDefault(i => i.Name == parts[1]);
                        if (targetDependencyPropertyField != null)
                            targetDependencyProperty = targetDependencyPropertyField.GetValue(null) as DependencyProperty;
                    }
                }
            }
            else
            {
                // it is a standard dependency property
                FieldInfo[] targetFields = targetElement.GetType().GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
                FieldInfo targetDependencyPropertyField = targetFields.FirstOrDefault(i => i.Name == targetPropertyName);

                if (targetDependencyPropertyField != null)
                    targetDependencyProperty = targetDependencyPropertyField.GetValue(null) as DependencyProperty;
            }


            // set binding
            if (targetDependencyProperty != null)
            {
                Binding binding = new Binding();
                binding.Source = sourceElement;
                if (!string.IsNullOrEmpty(bindingConfiguration.Path))
                { binding.Path = new PropertyPath(bindingConfiguration.Path); }
                binding.Mode = bindingConfiguration.BindingMode;
                binding.Converter = bindingConfiguration.Converter;
                binding.ConverterParameter = bindingConfiguration.ConverterParameter;
                binding.ConverterCulture = bindingConfiguration.ConverterCulture;
                binding.NotifyOnValidationError = bindingConfiguration.NotifyOnValidationError;
                binding.ValidatesOnExceptions = bindingConfiguration.ValidatesOnExceptions;

                // set the binding on our target element
                targetElement.SetBinding(targetDependencyProperty, binding);
            }
        }

        #endregion
    }


    /// <summary>
    /// Provides functionality to load any type with its class name, namespace and assembly-name within the Silverlight environment.
    /// </summary>
    /// <remarks>
    /// The Type.GetType method is different in Silverlight than in the standard .NET runtime. In Silverlight we have to provide the 
    /// fully qualified assembly name to get a type in a custom assembly. Only build in controls or types in the same assembly are 
    /// excluded from this rule. Full qualified assembly name means a syntax like the following: 
    /// MyComponent.MyType, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4bec85d7bec6698f.
    /// This class uses the XamlReader capability to resolve type during parsing a xaml-string. While this is a little time consuming
    /// the TypeLoader maintains a cache to get types faster.
    /// </remarks>
    public static class TypeLoader
    {
        // cache for resolved type
        private static Dictionary<string, Type> _cache = new Dictionary<string, Type>();

        /// <summary>
        /// Gets the System.Type with the specified name, name space and assembly name.
        /// </summary>
        /// <param name="className">The class name without namespace.</param>
        /// <param name="nameSpace">The name space</param>
        /// <param name="assemblyName">The name of the assembly containing the type.</param>
        /// <returns>The type matching the provided parameters or null if not found.</returns>
        //[DebuggerStepThrough()]
        public static Type GetType(string className, string nameSpace, string assemblyName)
        {
            // check
            if (StringHelper.IsNullOrWhiteSpace(nameSpace))
                return null;

            string xamlNamespace = string.Format("clr-namespace:{0}", nameSpace);
            // assembly name is optional
            if (!StringHelper.IsNullOrWhiteSpace(assemblyName))
                xamlNamespace += string.Format(";assembly={0}", assemblyName);

            return GetType(className, xamlNamespace);
        }

        /// <summary>
        /// Gets the System.Type with the specified name. 
        /// This method overload can be used for:
        /// 1. core controls such as Button, Grid, ListBox, etc. without specifying the namespace or assembly name.
        /// 2. with the qualified assembly name of the type without version and public key token like this: "MyNamespace.MyType, MyAssembly".
        /// </summary>
        /// <param name="className">Pure class name of Core Controls such as Button, Grid, ListBox, etc.</param>
        /// <returns>The type matching the provided parameters or null if not found.</returns>
        //[DebuggerStepThrough()]
        public static Type GetType(string className)
        {
            if (className != null && className.Contains(","))
            {
                string[] qualifiedNameParts = className.Split(',');

                if (qualifiedNameParts.Length == 2)
                {
                    string[] fullClassNameParts = qualifiedNameParts[0].Split('.');

                    if (fullClassNameParts.Length > 0)
                    {
                        // classname
                        string newClassName = fullClassNameParts.Last().Trim();

                        // namespace
                        string nameSpace = "";
                        for (int i = 0; i < fullClassNameParts.Length - 1; i++)
                        {
                            nameSpace += fullClassNameParts[i] + ".";
                        }
                        nameSpace = nameSpace.TrimEnd('.');

                        string assemblyName = qualifiedNameParts[1].Trim();

                        return GetType(newClassName, nameSpace, assemblyName);
                    }
                }

            }

            return GetType(className, "");
        }

        /// <summary>
        /// Gets the System.Type with the specified name. The xaml namespace specifies the namespace and assembly name in the same syntax as in xaml. 
        /// </summary>
        /// <param name="className">The class name without namespace.</param>
        /// <param name="xamlNamespace">
        /// The xaml namespace. This is the same syntax as used in XAML syntax. 
        /// Example: "clr-namespace:MyComponent.SubNamespace;assembly=MyAssemblyName
        /// </param>
        /// <returns>The type matching the provided parameters or null if not found.</returns>
        //[DebuggerStepThrough()]
        public static Type GetType(string className, string xamlNamespace)
        {
            // check input
            if (StringHelper.IsNullOrWhiteSpace(className))
                return null;

            if (className.Contains("."))
                throw new ArgumentException("className must not include the namespace. Please provide namespace with separate parameter.");

            // check if type is already in cache
            string key = xamlNamespace + "&" + className;

            if (_cache.ContainsKey(key))
                return _cache[key];


            lock (_cache)
            {
                try
                {
                    // check again because another thread might be faster and has already created the cache-entry
                    if (_cache.ContainsKey(key))
                        return _cache[key];

                    // create xaml with a simply Style element and set the TargetType property with the provided type name
                    string xaml = "<Style xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' ";

                    // set the xaml namesapce if provided
                    if (!StringHelper.IsNullOrWhiteSpace(xamlNamespace))
                    {
                        xaml += string.Format("xmlns:tmp='{0}' TargetType='tmp:{1}' />", xamlNamespace, className);
                    }
                    else
                    {
                        // Core controls such as Button, Grid, ListBox, etc do not need a namespace
                        xaml += string.Format("TargetType='{0}' />", className);
                    }

                    // let the XamlParser load the type via the TargetType property 
                    Style style = XamlReader.Load(xaml) as Style;

                    if (style != null)
                    {
                        Type targetType = style.TargetType;
                        AddToCache(key, targetType);
                        return targetType;
                    }
                }
                catch (Exception ex)
                {
                    // Try to load type in executing assembly
                    if (!StringHelper.IsNullOrWhiteSpace(xamlNamespace))
                    {
                        // note: Type.GetType uses needs assembly-qualified name of the type to get. If the type is 
                        //       in the currently executing assembly or in Mscorlib.dll, it is sufficient to supply 
                        //       the type name qualified by its namespace.
                        Type type = Type.GetType(string.Format("{0}.{1}", xamlNamespace.Replace("clr-namespace:", "").TrimEnd(';'), className));

                        if (type != null)
                        {
                            // add to cache
                            AddToCache(key, type);
                            return type;
                        }
                    }

                    //****** DONT SET VALUE TO NULL, BECAUSE OF CASES WHEN AN ASSEMBLY IS  *****
                    //****** LOADED DYNAMICALLY INTO THE APPLICATION DOMAIN                *****
                    // don't let the exception repeat. Set null as cache value
                    AddToCache(key, null);
                    //**************************************************************************/
                }
            }

            return null;
        }

        private static void AddToCache(string key, Type type)
        {
            _cache.Add(key, type);
            CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
            CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
        }

        static void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
            _cache.Clear();
        }

        private static class StringHelper
        {
            // helper because .NET 3.5 does not support IsNullOrWhiteSpace
            public static bool IsNullOrWhiteSpace(string str)
            {
                if (str == null)
                    return true;

                if (str.Trim() == string.Empty)
                    return true;

                return false;
            }
        }

    }
}
